package fi;

import java.nio.IntBuffer;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.badlogic.gdx.Application;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.Texture.TextureFilter;
import com.badlogic.gdx.graphics.Texture.TextureWrap;
import com.badlogic.gdx.graphics.glutils.FrameBuffer;
import com.badlogic.gdx.utils.BufferUtils;
import com.badlogic.gdx.utils.Disposable;
import com.badlogic.gdx.utils.GdxRuntimeException;

/**
 * <p>
 * Encapsulates OpenGL ES 2.0 frame buffer objects. This is a simple helper
 * class which should cover most FBO uses. It will automatically create a
 * texture for the color attachment and a renderbuffer for the depth buffer. You
 * can get a hold of the texture by {@link FrameBuffer#getColorBufferTexture()}.
 * This class will only work with OpenGL ES 2.0.
 * </p>
 * 
 * <p>
 * FrameBuffers are managed. In case of an OpenGL context loss, which only
 * happens on Android when a user switches to another application or receives an
 * incoming call, the framebuffer will be automatically recreated.
 * </p>
 * 
 * <p>
 * A FrameBuffer must be disposed if it is no longer needed
 * </p>
 * 
 * @author mzechner, kalle_h
 */
public class GFrameBuffer implements Disposable {
	/** the frame buffers **/
	private final static Map<Application, List<GFrameBuffer>> buffers = new HashMap<Application, List<GFrameBuffer>>();

	/** the color buffer texture **/
	private Texture colorTexture;

	/** the framebuffer handle **/
	private int framebufferHandle;

	/** the depthbuffer render object handle **/
	static public int depthbufferHandle;

	/** width **/
	private final int width;

	/** height **/
	private final int height;

	/** depth **/
	private final boolean hasDepth;

	/** format **/
	private final Pixmap.Format format;

	/**
	 * Creates a new FrameBuffer having the given dimensions and potentially a
	 * depth buffer attached.
	 * 
	 * @param format
	 *            the format of the color buffer
	 * @param width
	 *            the width of the framebuffer in pixels
	 * @param height
	 *            the height of the framebuffer in pixels
	 * @param hasDepth
	 *            whether to attach a depth buffer
	 * @throws GdxRuntimeException
	 *             in case the FraeBuffer could not be created
	 */
	public GFrameBuffer(Pixmap.Format format, int width, int height,
			boolean hasDepth) {
		this.width = width;
		this.height = height;
		this.format = format;
		this.hasDepth = hasDepth;
		build();
	}

	private void build() {
		if (!Gdx.graphics.isGL20Available())
			throw new GdxRuntimeException("GL2 is required.");

		colorTexture = new Texture(width, height, format);
		colorTexture.setFilter(TextureFilter.Linear, TextureFilter.Linear);
		colorTexture.setWrap(TextureWrap.ClampToEdge, TextureWrap.ClampToEdge);
		GL20 gl = Gdx.graphics.getGL20();

		IntBuffer handle = BufferUtils.newIntBuffer(1);
		gl.glGenFramebuffers(1, handle);
		framebufferHandle = handle.get(0);
		if (hasDepth && depthbufferHandle == 0) {
			gl.glGenRenderbuffers(1, handle);
			depthbufferHandle = handle.get(0);
		}

		gl.glBindTexture(GL20.GL_TEXTURE_2D,
				colorTexture.getTextureObjectHandle());

		if (hasDepth) {
			gl.glBindRenderbuffer(GL20.GL_RENDERBUFFER, depthbufferHandle);
			gl.glRenderbufferStorage(GL20.GL_RENDERBUFFER,
					GL20.GL_DEPTH_COMPONENT16, colorTexture.getWidth(),
					colorTexture.getHeight());
		}

		gl.glBindFramebuffer(GL20.GL_FRAMEBUFFER, framebufferHandle);
		gl.glFramebufferTexture2D(GL20.GL_FRAMEBUFFER,
				GL20.GL_COLOR_ATTACHMENT0, GL20.GL_TEXTURE_2D,
				colorTexture.getTextureObjectHandle(), 0);
		if (hasDepth) {
			gl.glFramebufferRenderbuffer(GL20.GL_FRAMEBUFFER,
					GL20.GL_DEPTH_ATTACHMENT, GL20.GL_RENDERBUFFER,
					depthbufferHandle);
		}
		int result = gl.glCheckFramebufferStatus(GL20.GL_FRAMEBUFFER);

		gl.glBindRenderbuffer(GL20.GL_RENDERBUFFER, 0);
		gl.glBindTexture(GL20.GL_TEXTURE_2D, 0);
		gl.glBindFramebuffer(GL20.GL_FRAMEBUFFER, 0);

		if (result != GL20.GL_FRAMEBUFFER_COMPLETE) {
			colorTexture.dispose();
			if (hasDepth) {
				handle.put(depthbufferHandle);
				handle.flip();
				gl.glDeleteRenderbuffers(1, handle);
			}

			colorTexture.dispose();
			handle.put(framebufferHandle);
			handle.flip();
			gl.glDeleteFramebuffers(1, handle);

			if (result == GL20.GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT)
				throw new IllegalStateException(
						"frame buffer couldn't be constructed: incomplete attachment");
			if (result == GL20.GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS)
				throw new IllegalStateException(
						"frame buffer couldn't be constructed: incomplete dimensions");
			if (result == GL20.GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT)
				throw new IllegalStateException(
						"frame buffer couldn't be constructed: missing attachment");
		}
	}

	/** Releases all resources associated with the FrameBuffer. */
	public void dispose() {
		GL20 gl = Gdx.graphics.getGL20();

		IntBuffer handle = BufferUtils.newIntBuffer(1);

		colorTexture.dispose();
		if (hasDepth) {
			handle.put(depthbufferHandle);
			handle.flip();
			gl.glDeleteRenderbuffers(1, handle);
		}

		handle.put(framebufferHandle);
		handle.flip();
		gl.glDeleteFramebuffers(1, handle);

		if (buffers.get(Gdx.app) != null)
			buffers.get(Gdx.app).remove(this);
	}

	/** Makes the frame buffer current so everything gets drawn to it. */
	public void begin() {
		Gdx.graphics.getGL20().glViewport(0, 0, colorTexture.getWidth(),
				colorTexture.getHeight());
		Gdx.graphics.getGL20().glBindFramebuffer(GL20.GL_FRAMEBUFFER,
				framebufferHandle);
	}

	/**
	 * Unbinds the framebuffer, all drawing will be performed to the normal
	 * framebuffer from here on.
	 */
	public void end() {
		Gdx.graphics.getGL20().glViewport(0, 0, Gdx.graphics.getWidth(),
				Gdx.graphics.getHeight());
		Gdx.graphics.getGL20().glBindFramebuffer(GL20.GL_FRAMEBUFFER, 0);
	}

	/** @return the color buffer texture */
	public Texture getColorBufferTexture() {
		return colorTexture;
	}

	/** @return the height of the framebuffer in pixels */
	public int getHeight() {
		return colorTexture.getHeight();
	}

	/** @return the width of the framebuffer in pixels */
	public int getWidth() {
		return colorTexture.getWidth();
	}
}
